package br.mikhas.util.date;

/**
 * Easy access to date logics and calculations.
 * <p>
 * This class is the base for all calculations to
 * <code>br.mikhas.util.date</code> package.
 * 
 * @author Mikhail Domanoski
 * 
 */
public final class DateCalculator {

	public final static int SUNDAY = 0;
	public final static int MONDAY = 1;
	public final static int TUESDAY = 2;
	public final static int WEDNESDAY = 3;
	public final static int THURSDAY = 4;
	public final static int FRIDAY = 5;
	public final static int SATURDAY = 6;

	/**
	 * The amount of milliseconds on a hour.
	 */
	private final static long MILLIS_IN_HOUR = 1000 * 60 * 60;

	/**
	 * The amount of milliseconds on a day.
	 */
	private final static long MILLIS_IN_DAY = MILLIS_IN_HOUR * 24;

	/**
	 * The amount of days on a leap year.
	 */
	private final static int LEAP_YEAR_DAY_COUNT = 366;

	/**
	 * The amount of days on a non-leap year.
	 */
	private final static int NON_LEAP_YEAR_DAY_COUNT = 365;

	/**
	 * The amount of milliseconds on a leap-year.
	 */
	private final static long LEAP_YEAR_MILLIS = LEAP_YEAR_DAY_COUNT
			* MILLIS_IN_DAY;

	/**
	 * The amount of milliseconds on a non-leap year.
	 */
	private final static long NON_LEAP_YEAR_MILLIS = NON_LEAP_YEAR_DAY_COUNT
			* MILLIS_IN_DAY;

	/**
	 * The number of days on each month on a leap year
	 */
	private final static int[] LEAP_YEAR_MONTH_DAYS = new int[] { 31, 29, 31,
			30, 31, 30, 31, 31, 30, 31, 30, 31 };

	/**
	 * The number of days on each month on a non-leap year
	 */
	private final static int[] NON_LEAP_YEAR_MONTH_DAYS = new int[] { 31, 28,
			31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

	private final static long[] LEAP_YEAR_MONTH_MILLIS = new long[12];

	private final static long[] NON_LEAP_YEAR_MONTH_MILLIS = new long[12];

	private static final long AVERAGE_MILLIS_IN_YEAR = (long) (365.5 * MILLIS_IN_DAY);

	/**
	 * Caches the amount of milliseconds from TIME ZERO to an year.
	 */
	private static long[] yearCache = new long[255];

	static {
		int month;
		long millis;

		// Calculate leap year month millis
		for (month = 0, millis = 0; month < 12; month++) {
			LEAP_YEAR_MONTH_MILLIS[month] = millis;
			millis += LEAP_YEAR_MONTH_DAYS[month] * MILLIS_IN_DAY;
		}

		// Calculate non-leap year month millis
		for (month = 0, millis = 0; month < 12; month++) {
			NON_LEAP_YEAR_MONTH_MILLIS[month] = millis;
			millis += NON_LEAP_YEAR_MONTH_DAYS[month] * MILLIS_IN_DAY;
		}
	}

	/**
	 * Checks if the year is a leap year.
	 * <p>
	 * Leap years are divisable by 400 and 4.
	 * <p>
	 * Years divisable by 100 are not leap.
	 * 
	 * @param year
	 *            The year
	 * @return If the year ia a leap year
	 */
	public static boolean isLeapYear(int year) {
		if (year % 400 == 0) {
			return true;
		} else if (year % 100 == 0) {
			return false;
		} else if (year % 4 == 0) {
			return true;
		}
		return false;
	}

	/**
	 * Get the amount of milliseconds from 1970 to the given year day 0.
	 * 
	 * @param year
	 *            The year
	 * @return The amount of milliseconds
	 */
	public static long getYearMillis(int year) {
		int y = 1970;
		if (year < y) {
			throw new IllegalArgumentException("Illegal year providen: " + year);
		}

		int yearIndex = year - y;
		long millis = yearCache[yearIndex];
		if (millis <= 0) {

			while (y < year) {
				millis += (isLeapYear(y) ? LEAP_YEAR_MILLIS
						: NON_LEAP_YEAR_MILLIS);
				y++;
			}

			yearCache[yearIndex] = millis;
		}

		return millis;
	}

	/**
	 * Get the amount of milliseconds from 1970 to the first day of a specified
	 * year and month.
	 * 
	 * @param year
	 *            The required year
	 * @param month
	 *            The month
	 * @return The milliseconds
	 */
	public static long getYearMonthMillis(int year, int month) {
		if (month < 1 || month > 12) {
			throw new IllegalArgumentException(
					"Month must be between 1 and 12. Given: " + month);
		}

		long millis = getYearMillis(year);
		month--;
		if (isLeapYear(year)) {
			millis += LEAP_YEAR_MONTH_MILLIS[month];
		} else {
			millis += NON_LEAP_YEAR_MONTH_MILLIS[month];
		}
		return millis;

	}

	/**
	 * Get the amount of milliseconds from 1970 to the first day of a specified
	 * year, month and day.
	 * 
	 * @param year
	 *            The required year
	 * @param month
	 *            The month
	 * @param day
	 *            The day
	 * @return The milliseconds
	 */
	public static long getYearMonthDayMillis(int year, int month, int day) {
		if (day <= 0) {
			throw new IllegalArgumentException("The day must be bigger than 0");
		}

		if (isLeapYear(year)) {
			if (day > LEAP_YEAR_MONTH_DAYS[month - 1]) {
				throw new IllegalArgumentException(
						"The day is bigger than the last day of this month");
			}
		} else {
			if (day > NON_LEAP_YEAR_MONTH_DAYS[month - 1]) {
				throw new IllegalArgumentException(
						"The day is bigger than the last day of this month");
			}
		}

		long millis = getYearMonthMillis(year, month);

		millis += ((day - 1) * MILLIS_IN_DAY);

		return millis;
	}

	/**
	 * Get the year from an instant.
	 * 
	 * @param instant
	 *            The instant
	 * @return The year
	 */
	public static int getYear(long instant) {
		int year = (int) (instant / AVERAGE_MILLIS_IN_YEAR);
		year += 1970;

		long yearMillis = getYearMillis(year);
		long yearNextMillis = getYearMillis(year + 1);

		// Year fix
		if (instant > yearMillis && instant < yearNextMillis) {
			return year;
		}

		return year + 1;
	}

	/**
	 * Gets the month of the year form a given instant.
	 * 
	 * @param instant
	 *            The instant the month is required
	 * @return The month of the year
	 */
	public static int getMonth(long instant) {
		return getMonth(instant, getYear(instant));
	}

	/**
	 * Gets the month of the year form a given instant.
	 * 
	 * @param instant
	 *            The instant the month is required
	 * @param year
	 *            The instant year to help the calculation
	 * @return The month of the year
	 */
	public static int getMonth(long instant, int year) {
		instant -= getYearMillis(year);

		long[] monthMillisList = isLeapYear(year) ? LEAP_YEAR_MONTH_MILLIS
				: NON_LEAP_YEAR_MONTH_MILLIS;

		for (int i = 1; i < 12; i++) {
			if (instant < monthMillisList[i]) {
				return i;
			}
		}
		return 12;
	}

	/**
	 * Gets the day of the month of a given instant
	 * 
	 * @param instant
	 *            The instant
	 * @param year
	 *            The instant year
	 * @param month
	 *            The instant month
	 * @return The day of the month
	 */
	public static int getDay(long instant, int year, int month) {
		instant -= getYearMonthMillis(year, month);
		return (int) (instant / MILLIS_IN_DAY) + 1;
	}

	/**
	 * Gets the day of the month of a given instant
	 * 
	 * @param instant
	 *            The instant
	 * @return The day of the month
	 */
	public static int getDay(long millis) {
		int year = getYear(millis);
		int month = getMonth(millis, year);
		return getDay(millis, year, month);
	}

	/**
	 * Gets the hour from a given instant.
	 * 
	 * @param millis
	 *            The instant
	 * @return The hour of the day
	 */
	public static int getHour(long millis) {
		int m = (int) (millis % MILLIS_IN_DAY);
		return (int) (m / MILLIS_IN_HOUR);
	}

	/**
	 * Gets the minutes from a given instant.
	 * 
	 * @param millis
	 *            The instant
	 * @return The minutes of the hour
	 */
	public static int getMinutes(long instant) {
		int m = (int) (instant % MILLIS_IN_HOUR);
		return m / (1000 * 60);
	}

	/**
	 * Gets the minutes from a given instant.
	 * 
	 * @param instant
	 *            The instant
	 * @return The seconds of the day
	 */
	public static int getSeconds(long instant) {
		int m = (int) (instant % (1000 * 60));
		return m / 1000;
	}

	/**
	 * Gets the milliseconds from a given instant
	 * 
	 * @param instant
	 * @return
	 */
	public static int getMillis(long instant) {
		return (int) (instant % 1000);
	}

	/**
	 * The month days to be used during the weekday calculation
	 */
	private static final int[] zellersMonths = { -1, 13, 14, 3, 4, 5, 6, 7, 8,
			9, 10, 11, 12 };

	/**
	 * Uses a given date to calculate the weekday and gets the number which
	 * represents it.
	 * <table>
	 * <tr>
	 * <th>Weekday
	 * <th>Number
	 * </tr>
	 * <tr>
	 * <td>Sunday
	 * <td>0
	 * </tr>
	 * <tr>
	 * <td>Monday
	 * <td>1
	 * </tr>
	 * <tr>
	 * <td>Tuesday
	 * <td>2
	 * </tr>
	 * <tr>
	 * <td>Wednesday
	 * <td>3
	 * </tr>
	 * <tr>
	 * <td>Thursday
	 * <td>4
	 * </tr>
	 * <tr>
	 * <td>Friday
	 * <td>5
	 * </tr>
	 * <tr>
	 * <td>Saturday
	 * <td>6
	 * </tr>
	 * </table>
	 * 
	 * @param year
	 *            The year of the date used to calculate the weekday
	 * @param month
	 *            The month of the year
	 * @param day
	 *            The day of the month
	 * @return A number representing the day of the week
	 */
	public static int getWeekday(int year, int month, int day) {
		int m = zellersMonths[month];
		year = month < 3 ? year - 1 : year;

		int aux = day;
		aux += ((m + 1) * 26) / 10;
		aux += year;
		aux += year / 4;
		aux += 6 * (year / 100);
		aux += year / 400;

		return (aux % 7);
	}

	/**
	 * Uses a given instant to calculate the weekday and gets the number which
	 * represents it.
	 * <p>
	 * <table>
	 * <tr>
	 * <th>Weekday
	 * <th>Number
	 * </tr>
	 * <tr>
	 * <td>Sunday
	 * <td>0
	 * </tr>
	 * <tr>
	 * <td>Monday
	 * <td>1
	 * </tr>
	 * <tr>
	 * <td>Tuesday
	 * <td>2
	 * </tr>
	 * <tr>
	 * <td>Wednesday
	 * <td>3
	 * </tr>
	 * <tr>
	 * <td>Thursday
	 * <td>4
	 * </tr>
	 * <tr>
	 * <td>Friday
	 * <td>5
	 * </tr>
	 * <tr>
	 * <td>Saturday
	 * <td>6
	 * </tr>
	 * </table>
	 * 
	 * @param instant
	 *            The instant to calculate the weekday
	 * @return A number representing the day of the week
	 */
	public static int getWeekday(long instant) {
		int year = getYear(instant);
		int month = getMonth(instant, year);
		int day = getDay(instant, year, month);

		return getWeekday(year, month, day);
	}
}
